/**
 * Copyright 1998-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
 * All rights reserved.
 */
package ca.uvic.cs.cogz.mapping.ui.renderer;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.swing.Icon;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.plaf.metal.MetalLookAndFeel;

import ca.uvic.cs.cogz.mapping.core.MappingTypeManager.MappingType;
import ca.uvic.cs.cogz.mapping.filter.TreeFilterManager;
import ca.uvic.cs.cogz.mapping.ontology.DataLoaderFacade;
import ca.uvic.cs.cogz.mapping.ontology.TransformationEngine;
import ca.uvic.cs.cogz.mapping.ui.tree.FilteredParentChildNode;
import ca.uvic.cs.cogz.mapping.ui.tree.ZoomManager;
import ca.uvic.cs.cogz.util.IconConstants;
import edu.stanford.smi.protege.model.Cls;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.model.Slot;
import edu.stanford.smi.protege.resource.Colors;
import edu.stanford.smi.protege.ui.FrameRenderer;
import edu.stanford.smi.protege.util.DefaultRenderer;
import edu.stanford.smi.protege.util.LazyTreeNode;
import edu.stanford.smi.protegex.owl.model.RDFProperty;
import edu.stanford.smi.protegex.owl.model.impl.DefaultOWLDatatypeProperty;
import edu.stanford.smi.protegex.owl.model.impl.DefaultOWLObjectProperty;

/**
 * Class for rendering ontology tree elements. This class is necessary for displaying search matches
 * with highlighted text, mapped items in bold when the filter is applied, and the "m" icon
 * representing a verified mapping.
 * 
 * @author Chris Callendar, Sean Falconer
 * @date 14-Nov-06
 */
public class MappingJTreeRenderer extends FrameRenderer implements IconConstants {
	private static final long serialVersionUID = 6068123968061405337L;

	private static final char WILDCARD = '*';
	private static final String SPECIAL = "[]{}()^+.?-:,&\\";

	/** static search parameters, these are static because I only need one copy per search */
	private static Pattern pattern = null;
	private static String queryString = null;
	private static String[] queryBits = null;

	/** reference to mapped frames for a given ontology */
	private Map<Frame, MappingType> mappedFrames;

	/** reference to ZoomManager for calculating the text size of tree elements */
	private ZoomManager zoomManager;

	private Color matchColor = Color.blue;
	private Font oldFont;
	private FontMetrics oldMetrics;
	private Color oldColor;

	private boolean bold = true;

	/** the search index value, either source or target */
	private int searchIndex;

	public MappingJTreeRenderer(Map<Frame, MappingType> mappedFrames, ZoomManager zoomManager, int searchIndex) {
		this.mappedFrames = mappedFrames;
		this.zoomManager = zoomManager;
		this.searchIndex = searchIndex;
	}
	
	/**
	 * Loads a given LazyTreeNode for rendering.
	 * @param lazyTreeNode
	 */
	protected void loadCls(LazyTreeNode lazyTreeNode) {
		if (lazyTreeNode.getUserObject() instanceof Frame) {
			Frame frame = (Frame) lazyTreeNode.getUserObject();
			
			setMainIcon(getIcon(lazyTreeNode));
			
			try {
				String frameName = frame.getBrowserText();
				if(frameName == null) {
					frameName = frame.getName();
				}
				frameName = cleanFrameName(frameName);
				
				setMainText(frameName);
				if (frame instanceof Cls) {
					appendText(getInstanceCountString((Cls) frame));
				}
				else if(frame instanceof RDFProperty) { // append the data type
					RDFProperty property = (RDFProperty)frame;
					appendText(":" + cleanFrameName(property.getRange().getBrowserText()));
				}
				// appendType(cls);
				setBackgroundSelectionColor(Colors.getClsSelectionColor());
			} catch(Exception e) {}
		}
		else if(lazyTreeNode.getUserObject() instanceof List) {
			setMainText("Slots");
		}
	}

	protected Icon getIcon(LazyTreeNode lazyTreeNode) {
		if(lazyTreeNode.getUserObject() instanceof Cls) {
			Cls cls = (Cls) lazyTreeNode.getUserObject();
	
			if (cls.isAbstract()) {
				return zoomManager.getScaledIcon(ZoomManager.ICON_CLS_ABSTRACT, lazyTreeNode);
			} else if (cls.isClsMetaCls()) {
				return zoomManager.getScaledIcon(ZoomManager.ICON_CLS_METADATA, lazyTreeNode);
			}
			
			return zoomManager.getScaledIcon(ZoomManager.ICON_CLS, lazyTreeNode);
		}
		else if(lazyTreeNode.getUserObject() instanceof Slot) {
			Slot slot = (Slot)lazyTreeNode.getUserObject();
			if(slot instanceof DefaultOWLDatatypeProperty) {
				return zoomManager.getScaledIcon(ZoomManager.ICON_DATATYPE_SLOT, lazyTreeNode);
			}
			else if(slot instanceof DefaultOWLObjectProperty) {
				return zoomManager.getScaledIcon(ZoomManager.ICON_OBJECT_SLOT, lazyTreeNode);
			}
			return zoomManager.getScaledIcon(ZoomManager.ICON_DEFAULT_SLOT, lazyTreeNode);
		}
		
		return null;
	}

	/**
	 * Overroad this method in order to pass the actual LazyTreeNode to the load method rather than
	 * the Cls UserObject.
	 */
	@Override
	protected Component setup(Component c, Object value, boolean hasFocus, boolean isSelected) {
		_grayedText = false;
		_value = value;
		Font font = c.getFont();
		if (font.isBold()) {
			font = font.deriveFont(Font.PLAIN);
		}
		setFont(font);
		_hasFocus = hasFocus;
		_isSelected = isSelected;

		_elements.clear();
		if (value == null) {
			loadNull();
		} else if (value instanceof LazyTreeNode) {
			LazyTreeNode node = (LazyTreeNode) value;
			Object object = node.getUserObject();
			if (node.isDuplicate()) {
				loadDuplicate(object);
			} else {
				load(node);
			}
		} else {
			load(value);
		}

		_fontMetrics = getFontMetrics(getFont());
		LookAndFeel currentLookAndFeel = UIManager.getLookAndFeel();
		if (currentLookAndFeel != _cachedLookAndFeel) {
			loadTreeColors();
			_cachedLookAndFeel = currentLookAndFeel;
			if (_cachedLookAndFeel instanceof MetalLookAndFeel) {
				_focusRectColor = MetalLookAndFeel.getFocusColor();
			} else {
				_focusRectColor = Color.GRAY;
			}
		}
		checkDropTarget(c, value);
		return this;
	}
	
	/**
	 * Removes the namespace information from the frame name.
	 */
	private String cleanFrameName(String frameName) {
		if(frameName.startsWith("http:")) {
			frameName = frameName.substring(frameName.indexOf("#")+1);
		}
		return frameName;
	}

	private void setupSearchRendering() {
		String searchText = TreeFilterManager.getInstance().getSearchText(searchIndex);

		if (searchText.length() > 0) {
			if (queryString == null || !queryString.equals(searchText)) {
				queryString = searchText.toLowerCase();
				queryBits = queryString.split("\\*");
				buildPattern();
			}
		} else {
			queryString = null;
			pattern = null;
		}
	}

	private void buildPattern() {
		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < queryString.length(); ++i) {
			char c = queryString.charAt(i);
			if (c == WILDCARD) {
				buffer.append(".*");
			} else if (SPECIAL.indexOf(c) != -1) {
				buffer.append('\\');
				buffer.append(c);
			} else {
				buffer.append(c);
			}
		}
		pattern = Pattern.compile(buffer.toString(), Pattern.CASE_INSENSITIVE);
	}

	/**
	 * Attempts to append the appropriate icon to the end of the treeNode's text if it is a
	 * non-filtered node and also corresponds to a mapped/candidate item.
	 */
	private void setupAppendIcon(LazyTreeNode treeNode) {
		if (!(treeNode.getUserObject() instanceof Frame)) {
			return;
		}

		Frame frame = (Frame) treeNode.getUserObject();

		MappingType mappingType = mappedFrames.get(frame);
		
		if (mappingType != null) {
			boolean isVisible = DataLoaderFacade.getInstance().getMappingCollection().getMappingTypeManager().isVisible(mappingType);

			// only display icon if this mapping type is visible
			if (isVisible) {
				// check to see if this frame should be filtered
				
				boolean isFiltered = DataLoaderFacade.getInstance().getMappingCollection().isFiltered(frame);
				if (!isFiltered) {
					MappingType activeFilterMappingType = TreeFilterManager.getInstance().getActiveMappingTypeFilter(searchIndex);

					// add appended icons based on mapping type
					if ((activeFilterMappingType == null || activeFilterMappingType == mappingType) && mappingType == MappingType.VERIFIED_MAPPING) {
						appendIcon(zoomManager.getScaledIcon(ZoomManager.ICON_MAPPED, treeNode));
					} else if ((activeFilterMappingType == null || activeFilterMappingType == mappingType) && mappingType == MappingType.TEMPORARY_MAPPING) {
						appendIcon(zoomManager.getScaledIcon(ZoomManager.ICON_TEMPORARILY_MAPPED, treeNode));
					} else if ((activeFilterMappingType == null || activeFilterMappingType == mappingType) && mappingType == MappingType.CANDIDATE_MAPPING) {
						appendIcon(zoomManager.getScaledIcon(ZoomManager.ICON_CANDIDATE, treeNode));
					}

					// set the font style to bold if this is a filter activated item
					if (activeFilterMappingType != null && activeFilterMappingType == mappingType) {
						setFontStyle(Font.BOLD);
					}
					
					if(treeNode instanceof FilteredParentChildNode && ((FilteredParentChildNode)treeNode).isMappingFiltered()) {
						appendText("->  ");
					}
				}
			}
		}

		if(treeNode instanceof FilteredParentChildNode) {
			// check to see if this node has any filtered children
			int normalChildCount = ((FilteredParentChildNode) treeNode).getAllChildrenCount();
			int filteredChildCount = ((FilteredParentChildNode) treeNode).getChildCount();
			if (filteredChildCount < normalChildCount) {
				addText("[" + (normalChildCount - filteredChildCount) + " filtered]");
			}
	
			// check if frame is pinned
			if (TreeFilterManager.getInstance().isFramePinned(searchIndex, frame)) {
				appendIcon(zoomManager.getScaledIcon(ZoomManager.ICON_PINNED, treeNode));
			}
			
			// check if frame has been instantiated in a transformation
			//if(TransformationEngine.getInstance().getInstatiatedClasses().contains(frame)) {
			//	appendIcon(zoomManager.getScaledIcon(ZoomManager.ICON_INSTATIATED, treeNode));
			//}
		}
	}

	@Override
	public void load(Object value) {
		LazyTreeNode treeNode = null;
		
		if (value instanceof LazyTreeNode) {
			treeNode = (LazyTreeNode) value;
		} else {
			return;
		}

		// load icons and text
		loadCls(treeNode);

		setupSearchRendering();
		setFont(zoomManager.getScaledFont(treeNode));
		_fontMetrics = getFontMetrics(getFont());

		setupAppendIcon(treeNode);
	}

	/**
	 * Overrides the {@link DefaultRenderer#paintString(Graphics, String, Point, Color, Dimension)}
	 * method to bold part of the text that matches the query string. If no match is found then the
	 * super method is called.
	 */
	@Override
	protected void paintString(Graphics g, String text, Point position, Color color, Dimension size) {
		boolean callSuper = true;
		if (queryString != null) {
			color = Color.black;
			// this code is copied from the super method in DefaultRenderer
			if (color != null) {
				g.setColor(color);
			}

			int y = (size.height + _fontMetrics.getAscent()) / 2 - 2; // -2 is a bizarre fudge
			// factor that makes it look
			// better!

			if (pattern.matcher(text).find()) {
				doPatternColoring(g, text, position, y);
				callSuper = false;
			}
		}

		// call the super method if we didn't paint the string already
		if (callSuper) {
			super.paintString(g, text, position, color, size);
		}
	}

	/**
	 * Performs the coloring of tree string text based on the search query information.
	 */
	private void doPatternColoring(Graphics g, String text, Point position, int y) {
		String textValue = text.toLowerCase();
		int prev = 0;

		if (queryBits[0].length() > 0 && textValue.indexOf(queryBits[0]) != 0) {
			paintString(g, text, position, y, false);
		} else {
			boolean highlightOn = false;
			// process each query bit and color the match
			for (int i = 0; i < queryBits.length; i++) {
				if (queryBits[i] != null && queryBits[i].length() > 0) {
					int start = textValue.indexOf(queryBits[i], prev);
					if (start > prev) {
						highlightOff(g);
						highlightOn = false;
						String extra = text.substring(prev, start);
						paintString(g, extra, position, y, false);
					}
					if (start >= 0) {
						String match = text.substring(start, start + queryBits[i].length());
						if (!highlightOn) {
							highlightOn(g, matchColor);
							highlightOn = true;
						}
						paintString(g, match, position, y, true);
						prev = start + queryBits[i].length();
					}
				}
			}

			// paint the left over bit
			if (prev < text.length()) {
				highlightOff(g);
				String extra = text.substring(prev, text.length());
				paintString(g, extra, position, y, false);
			}
		}
	}

	private void highlightOn(Graphics g, Color highlightColor) {
		// set the font and color
		if (bold) {
			oldFont = g.getFont();
			g.setFont(oldFont.deriveFont(Font.BOLD));
			oldMetrics = _fontMetrics;
			_fontMetrics = g.getFontMetrics();
		}
		if (highlightColor != null) {
			oldColor = g.getColor();
			g.setColor(highlightColor);
		}
	}

	/** Restore the original font and color. */
	private void highlightOff(Graphics g) {
		if (oldFont != null) {
			g.setFont(oldFont);
		}
		if (oldMetrics != null) {
			// _fontMetrics = oldMetrics;
		}
		if (oldColor != null) {
			g.setColor(oldColor);
		}
	}

	/**
	 * Paints the string at the given position.
	 * 
	 * @param text the string to paint
	 * @param position the x-position for the string (after painting the value is updated to the
	 *            next x-position)
	 * @param y the y-position for the string
	 * @param highlight if the string should be bolded and/or colored
	 */
	protected void paintString(Graphics g, String text, Point position, int y, boolean highlight) {
		if (text.length() > 0) {
			g.drawString(text, position.x, y);
			position.x += _fontMetrics.stringWidth(text);
		}
	}

	@SuppressWarnings("unchecked")
	public Dimension getPreferredSize() {
		Dimension d = new Dimension(0, _fontMetrics.getHeight());
		Object previousElement = null;
		Iterator i = _elements.iterator();
		while (i.hasNext()) {
			Object element = i.next();
			d.width += getGap(previousElement, element);
			updateSize(d, element);
			previousElement = element;
		}

		return d;
	}

	private int getGap(Object previousElement, Object currentElement) {
		int gap = 0;
		if (previousElement == null) {
			gap = currentElement instanceof String ? 3 : 0;
		} else if (previousElement instanceof Icon) {
			if (currentElement instanceof Icon) {
				gap = 0;
			} else {
				gap = 3;
			}
		}
		return gap;
	}

	private void updateSize(Dimension d, Object element) {
		if (element instanceof Icon) {
			updatePreferredSize(d, (Icon) element);
		} else if (element instanceof String) {
			updatePreferredSize(d, (String) element);
		}
	}

	private void updatePreferredSize(Dimension d, Icon icon) {
		d.width += icon.getIconHeight();
		d.height = Math.max(d.height, icon.getIconHeight());
	}

	private void updatePreferredSize(Dimension d, String text) {
		d.width += _fontMetrics.stringWidth(text) + 5;
	}
}